import copy
import re

import rltext
import random

from erlglobals import *

def randomRoll(n):
	return ( random.randint(1, n) + random.randint(1, n) ) // 2

def randomGauss(sigma, clip, mu = 0):
	while True:
		rv = random.gauss( mu=0, sigma=sigma )
		if abs(rv) < clip:
			break
	return mu + rv

def randomGaussModify(n, sigma = 5.0):
	n = int(0.5 + n *randomGauss(clip=0.5, sigma = sigma, mu = 1.0 ) )
	return n


def randomDirection():
	rv = (0,0)
	while rv == (0,0):
		rv = random.randint(-1,1), random.randint(-1,1)
	return rv

def randomString():
		# the usage of this is of course a monumental hack.
		# there's a miniscule but nonzero chance of it causing
		# problems unless used with care(appropriate prefixes etc.)
		# but realistically, random 20-character strings 
		# are NOT going to collide.
	import string
	alpha = string.letters + string.digits
	rv = []
	for i in range(20):
		rv.append( random.choice( alpha ) )
	return "".join( rv )


class ProgrammerGoof (Exception):
	def __init__(self, s): self.s = s
	def __str__(self): return s

class Colour:
	# constant container
	
	# standard curses colours
	black = "undefined"
	blue = "undefined"
	cyan = "undefined"
	green = "undefined"
	magenta = "undefined"
	red = "undefined"
	white = "undefined"
	yellow = "undefined"

class Item:
	def __init__(self, article, name):
		self.article = article
		self.name = name
	def articledName(self):
		return " ".join( [self.article, self.name] )

class MobileVision:
	def __init__(self, mobile, radius = None):
		self.mobile = mobile
		self.radius = radius
	def __call__(self, x, y):
		if self.radius:
			dx, dy = self.mobile.x-x,self.mobile.y-y
			if dx*dx+dy*dy > self.radius*self.radius: return False
		try:
			return self.mobile.level.tiles[x][y].type.obstruction in self.mobile.seethrough
		except IndexError:
			return False

class IndependentVision:
	def __init__(self, level, origin, seethrough, radius = None):
		self.level = level
		self.origin = origin
		self.seethrough = seethrough
		self.radius = radius
	def __call__(self, x, y):
		if self.radius:
			x0,y0 = self.origin
			if self.radius**2 < (x-x0)*(x-x0) + (y-y0)*(y-y0):
				return False
		try:
			return self.level.tiles[x][y].type.obstruction in self.seethrough
		except IndexError:
			return False

class Mobile:
	serializedAttributes = [
		"name",
		"glyph",
		"level",
		"x",
		"y",
		"mobility",
		"seethrough",
		"mayDisplace",
		"inventory",
		"facing",
		"hp",
		"maxHp",
		"meleeAttack",
		"frozenCountdown"
	]
	def __init__(self, name, glyph, mobility, maxHp, seethrough = [ "clear" ]):
		self.hp = self.maxHp = maxHp
		self.name = name
		self.glyph = glyph
		self.level = None
		self.x, self.y = None, None
		self.mobility = mobility
		self.seethrough = seethrough
		self.mayDisplace = False
		from erlitem import Inventory
		self.inventory = Inventory() # ERL
		self.vision = MobileVision( self )
		self.meleeAttack = None
		self.frozenCountdown = 0
	def restore(self):
		self.vision = MobileVision( self )
	def highlightFacing(self, interface):
		from rlfov import Wheel, WheelIndex
		dx, dy = self.facing
		interface.highlight( self.x + dx, self.y + dy )
		i = WheelIndex( self.facing )
		for dx, dy in [ Wheel[j] for j in range(i-1,i+2) ]:
			interface.highlight( self.x + dx, self.y + dy )
	def descriptiveName(self): # "Peter, level 64 Barbarian", "albatross"
		return self.name
	def definiteName(self): # "Peter", "the albatross"
		return self.name
	def swap(self, that):
		self.x, self.y, that.x, that.y = that.x, that.y, self.x, self.y
		self.level, that.level = that.level, self.level # not recommended to actually use this
		self.level.tiles[self.x][self.y].mobile = self
		that.level.tiles[that.x][that.y].mobile = that
		self.didMove()
		that.didMove()
	def move(self, level, x, y, noTriggerDidMove = False):
		if self.level:
			assert self.x != None and self.y != None
			assert self.level.tiles[self.x][self.y].mobile == None or self.level.tiles[self.x][self.y].mobile == self
			self.level.tiles[self.x][self.y].mobile = None
		self.level, self.x, self.y = level, x, y
		level.tiles[x][y].mobile = self
		self.facing = 0,0
		while self.facing == (0,0):
			import random
			self.facing = random.randint(-1,1), random.randint(-1,1)
		self.updateItems()
		self.inactivity = 0 # this is a hack: only matters, AND ONLY SAVED, for nonpc
		if not noTriggerDidMove:
			self.didMove()
	def didMove(self):
		pass
	def moveRelative(self, dv):
		dx, dy = dv
		self.move( self.level, self.x + dx, self.y + dy, noTriggerDidMove = True )
		self.facing = dv
		self.didMove()
	def mayMove(self, tile):
		if not tile: return False
		if tile.mobile != None:
			return False
		if not tile.type.traversibility in self.mobility:
			return False
		return True
	def mayMoveRelative(self, dv):
		return self.mayMove( self.getTileRelative(dv) )
	def getTileRelative(self, dv):
		dx, dy = dv
		try:
			return self.level.tiles[self.x+dx][self.y+dy]
		except IndexError:
			return None
	def updateItems(self):
		for item in self.inventory:
			item.x = self.x
			item.y = self.y
			item.facing = randomDirection()
			item.level = self.level
	def giveItem(self, item):
		try:
			returned, taken = self.inventory.add( item )
			if not returned:
				return
		except NoMoreSlots:
			pass
		self.level.putItem( returned, self.x, self.y )
	def distance(self, mobile):
		from math import sqrt
		if mobile.level != self.level:
			return almostInfinity
		dx, dy = mobile.x-self.x, mobile.y-self.y
		return sqrt( dx*dx+dy*dy )
	def isAdjacent(self, mobile):
		dx, dy = mobile.x-self.x, mobile.y-self.y
		return max( abs(dx), abs(dy) ) <= 1
	def damageMessage(self, amount):
		pass
	def damage(self, amount):
		self.damageMessage(amount)
		self.hp -= amount
		if self.hp <= 0:
			self.die()
	def attackMelee(self, target):
		import random
		msgHit, msgMiss, hitchance, damage = self.meleeAttack
		damage = randomRoll( damage )
		if random.random() < hitchance:
			self.game.reportAttack( self, target, msgHit )
			target.damage( damage )
		else:
			self.game.reportAttack( self, target, msgMiss )

class Glyph:
	def __init__(self, char = None, fgColour = None, bgColour = None, effects = []):
		self.char = char
		self.fgColour = fgColour
		self.bgColour = bgColour
		self.effects = []
	def collage(self, foreground):
		rv = Glyph( foreground.char, foreground.fgColour, foreground.bgColour )
		if not rv.bgColour: rv.bgColour = self.bgColour
		return rv
	def recolorize(self, fg, bg):
		return Glyph( self.char, fg, bg )
	def restore(self): pass

class Tile:
	serializedAttributes = [
		"type",
		"items",
		"mobile", # nonpc, ((pushable)), playercharacter
		"remembered",
		"pcDist",
		"pcDistDalek"
	]
	def __init__(self, tileType):
		from erlitem import Inventory
		self.type = tileType
		self.items = Inventory()
		self.mobile = None # only one mobile
		self.remembered = False
		self.pcDist = None
	def description(self):
		things = [ item.articledName() for item in self.items ]
		if self.mobile: things.append( self.mobile.descriptiveName() )
		if things: return rltext.makeEnglishList( things ) + " on " + self.type.name
		return self.type.name
	def appearance(self, showMobiles = True, showItems = True):
		rv = copy.copy( self.type.glyph )
		if showMobiles and self.mobile: rv = rv.collage( self.mobile.glyph )
		elif showItems and len(self.items) > 0: rv = rv.collage( self.items[-1].glyph )
		return rv

class TileType:
	def __init__(self, name, traversibility, obstruction, glyph):
		self.name = name
		self.traversibility = traversibility # string, e.g. "void", "solid", "water", "land". no mobile should move through "impassable".
		self.obstruction = obstruction # same as traversibility, but for vision. no mobile should see through "opaque".
		self.glyph = glyph
	def spawn(self, tileClass = Tile):
		return tileClass( self )

class Level:
	def __init__(self, width, height, backgroundTileType = None):
		self.width = width
		self.height = height
		self.tiles = [ [ None for j in range(self.height) ] for i in range(self.width) ]
		if backgroundTileType:
			for y in range(self.height):
				for x in range(self.width):
					self.tiles[x][y] = backgroundTileType.spawn()
	def getRandomMovableTile(self, mobile):
		import random
		rv = []
		for y in range(self.height):
			for x in range(self.width):
				if mobile.mayMove( self.tiles[x][y] ):
					rv.append( (x,y) )
		return random.choice( rv )
	def getRandomTileOfType(self, tileType):
		import random
		rv = []
		for y in range(self.height):
			for x in range(self.width):
				if self.tiles[x][y].type == tileType:
					rv.append( (x,y) )
		return random.choice( rv )
	def putItem(self, item, x, y):
		self.tiles[x][y].items.add( item )
		item.level = self
		item.facing = randomDirection()
		item.x = x
		item.y = y
		#?# item.controller = None
	def relocateMobileAt(self, x, y, freett ):
		mob = self.tiles[x][y].mobile
		if not mob: return
		actF = getattr( mob, "act", None )
		if callable(actF):
			for i in range(100):
				actF()
				if self.tiles[x][y].mobile == None:
					return
		okayPlacesToBe = []
		for dx, dy in [ (dx,dy) for dx in (-1,0,1) for dy in (-1,0,1) if dx or dy ]:
			nx, ny = x+dx, y+dy
			if mob.mayMove( self.tiles[nx][ny] ):
				okayPlacesToBe.append( (nx,ny) )
		if okayPlacesToBe:
			nx, ny = random.choice( okayPlacesToBe )
			mob.move( self, nx, ny )
			return
		for i in range(100):
			nx, ny = self.getRandomTileOfType( freett )
			if x == nx and y == ny: continue
			mob.move( self, nx, ny )
			return
		self.tiles[x][y].mobile = None # Just zap it, let's hope it wasn't anything important!
	def clearDistances(self):
		for x in range(self.width):
			for y in range(self.height):
				self.tiles[x][y].pcDist = None
				self.tiles[x][y].pcDistDalek = None
	def growDistancesFrom(self, x0, y0, alternate = False):
		import heapq
		q = []
		c = set( [(x0,y0)] )
		ad = [ (dx,dy) for dx in (-1,0,1) for dy in (-1,0,1) if dx or dy ]
		ts = set([
			"closed-door",
			"open-door",
			"land",
		]) # note that we path right through closed doors, so seekers should be
		  # able to open or destroy them. this is a no-dalek zone (okay, that's
		  # stairs)
		if alternate:
			ts.remove( "closed-door" )
		import time
		t0 = time.time()
		heapq.heappush( q, (0,x0,y0) )
		while q:
			d,x,y = heapq.heappop( q )
			if alternate:
				self.tiles[x][y].pcDistDalek = d
			else:
				self.tiles[x][y].pcDist = d
			for dx, dy in ad:
				nx, ny = x+dx, y+dy
				t = self.tiles[nx][ny].type.traversibility
				if not t in ts: continue
				if (nx,ny) in c: continue
				heapq.heappush( q, (d+1,nx,ny) )
				c.add( (nx,ny) )
